首先建立裝載角色資料的 ViewModel,因為接下來的權限會以角色判斷,ASP.NET Core Identity 乘載角色的 Model 為 IdentityRole,裡面有太多不該讓使用者看到的資訊,通常會自己寫新的 ViewModel 以過濾多於資訊,這邊只呈現 RoleId、RoleName 跟 Role 底下所有使用者的名稱。
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace BlazorServer.ViewModels
{
    public class CustomRoleViewModel
    {
        public CustomRoleViewModel()
        {
            Users = new();
        }
        public string RoleId { get; set; }
        [Required(ErrorMessage = "角色名稱為必填")]
        public string RoleName { get; set; }
        public List<string> Users { get; set; }
    }
}
建立IRolesRepository.cs跟RolesRepository.cs,這是專門處理角色的 Service,把基本的角色 CRUD(Create, Read, Update, Delete) 功能實作,再去Startup.cs註冊。
介面IRolesRepository.cs
using BlazorServer.ViewModels;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace BlazorServer.Services
{
    public interface IRolesRepository
    {
        Task<CustomRoleViewModel> GetRoleAsync(string RoleId);
        Task<List<CustomRoleViewModel>> GetRolesAsync();
        Task<ResultViewModel> CreateRoleAsync(CustomRoleViewModel model);
        Task<ResultViewModel> EditRoleAsync(CustomRoleViewModel model);
        Task<ResultViewModel> DeleteRoleAsync(string roleId);
        Task<List<CustomUserRoleViewModel>> EditUsersInRoleAsync(string RoleId);
        Task<ResultViewModel> EditUsersInRoleAsync(List<CustomUserRoleViewModel> model, string RoleId);
    }
}
實作RolesRepository.cs,這邊注入的RoleManager跟UserManager是 ASP.NET Core Identity 預設處理角色跟使用者的 Service,之前在Startup.cs寫的services.AddIdentity<IdentityUser, IdentityRole>()…就註冊了該功能,底下有各式 Role、User 相關API可以呼叫。
using BlazorServer.ViewModels;
using Microsoft.AspNetCore.Identity;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace BlazorServer.Services
{
    public class RolesRepository : IRolesRepository
    {
        private readonly RoleManager<IdentityRole> _roleManager;
        private readonly UserManager<IdentityUser> _userManager;
        public RolesRepository(RoleManager<IdentityRole> roleManager,
            UserManager<IdentityUser> userManager)
        {
            _roleManager = roleManager;
            _userManager = userManager;
        }
        #region Roles
		// 取得單一角色
        public async Task<CustomRoleViewModel> GetRoleAsync(string RoleId)
        {
            var role = await _roleManager.FindByIdAsync(RoleId);
            var users = await _userManager.GetUsersInRoleAsync(role.Name);
            var result = new CustomRoleViewModel
            {
                RoleId = role.Id,
                RoleName = role.Name,
                Users = users.Select(u => u.UserName).ToList()
            };
            return result;
        }
		// 取得角色 List
        public async Task<List<CustomRoleViewModel>> GetRolesAsync()
        {
            var roles = _roleManager.Roles;
            var customRoles = new List<CustomRoleViewModel>();
            foreach (var role in roles)
            {
                customRoles.Add(new CustomRoleViewModel { RoleId = role.Id, RoleName= role.Name });
            }
            return await Task.Run(() => customRoles);
        }
		// 建立角色
        public async Task<ResultViewModel> CreateRoleAsync(CustomRoleViewModel model)
        {
            IdentityRole identityRole = new IdentityRole
            {
                Name = model.RoleName
            };
            var result = await _roleManager.CreateAsync(identityRole);
            if (result.Succeeded)
            {
                return new ResultViewModel
                {
                    Message = "角色建立成功!",
                    IsSuccess = true
                };
            }
            return new ResultViewModel
            {
                Message = "角色建立失敗!",
                IsSuccess = false
            };
        }
		// 編輯角色
        public async Task<ResultViewModel> EditRoleAsync(CustomRoleViewModel model)
        {
            var role = await _roleManager.FindByIdAsync(model. RoleId);
            if (role == null)
            {
                return new ResultViewModel
                {
                    Message = $"找不到 Id 為 {model.RoleId} 的角色",
                    IsSuccess = false
                };
            }
            role.Name = model.RoleName;
            var result = await _roleManager.UpdateAsync(role);
            if (result.Succeeded)
            {
                return new ResultViewModel
                {
                    Message = "角色更新成功!",
                    IsSuccess = true
                };
            }
            return new ResultViewModel
            {
                Message = "角色更新失敗!",
                IsSuccess = false
            };
        }
		// 刪除角色
        public async Task<ResultViewModel> DeleteRoleAsync(string roleId)
        {
            var role = await _roleManager.FindByIdAsync(roleId);
            if (role == null)
            {
                return new ResultViewModel
                {
                    Message = $"找不到 Id 為 {roleId} 的角色",
                    IsSuccess = false
                };
            }
            var result = await _roleManager.DeleteAsync(role);
            if (result.Succeeded)
            {
                return new ResultViewModel
                {
                    Message = "角色刪除成功!",
                    IsSuccess = true
                };
            }
            return new ResultViewModel
            {
                Message = "角色刪除失敗!",
                IsSuccess = false
            };
        }
        #endregion
    }
}
現在有處理資料的功能了,接下來要產生畫面。RolesManagement.razor.cs
using BlazorServer.Services;
using BlazorServer.Shared;
using BlazorServer.ViewModels;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading.Tasks;
namespace BlazorServer.Pages.RolesManagement
{
    public partial class RolesManagement
    {
        [Inject] protected IRolesRepository RolesRepository { get; set; }
        [Inject] protected IJSRuntime js { get; set; }
        private JsInteropClasses jsClass;
        public List<CustomRoleViewModel> Roles { get; set; } = new();
        protected override async Task OnInitializedAsync()
        {
            await loadData();
            jsClass = new(js);
        }
        private async Task loadData()
        {
            Roles = await RolesRepository.GetRolesAsync();
        }
        private async Task editRole(string roleId)
        {
            NavigationManager.NavigateTo($"RolesManagement/EditRole/{roleId}");
        }
        private async Task deleteRole(string roleId)
        {
            SweetConfirmViewModel sweetConfirm = new SweetConfirmViewModel()
            {
                RequestTitle = $"是否確定刪除角色{roleId}?",
                RequestText = "這個動作不可復原",
                ResponseTitle = "刪除成功",
                ResponseText = "角色被刪除了",
            };
            string jsonString = JsonSerializer.Serialize(sweetConfirm);
            bool result = await jsClass.Confirm(jsonString);
            if (result)
            {
                var deleted = await RolesRepository.DeleteRoleAsync(roleId);
                if (deleted.IsSuccess)
                {
                    await loadData();
                }
                else
                {
                    await jsClass.Alert(deleted.Message);
                }
            }
        }
    }
}
RolesManagement.razor
@page "/RolesManagement/RolesList"
@attribute [Authorize]
<h1>所有角色</h1>
@if (Roles.Any())
{
    <NavLink class="btn btn-primary mb-3" href="RolesManagement/CreateRole" Match="NavLinkMatch.All">
        新增角色
    </NavLink>
    foreach (var role in Roles)
    {
        <div class="card mb-3 w-25">
            <div class="card-header">
                Role Id : @role.RoleId
            </div>
            <div class="card-body">
                <h5 class="card-title">@role.RoleName</h5>
            </div>
            <div class="card-footer">
                <button type="button" class="btn btn-primary" @onclick="()=>editRole(role.RoleId)">
                    編輯角色
                </button>
                <button type="button" class="btn btn-danger" @onclick="()=>deleteRole(role.RoleId)">
                    刪除角色
                </button>
            </div>
        </div>
    }
}
else
{
    <div class="card w-25">
        <div class="card-header">
            還沒有角色
        </div>
        <div class="card-body">
            <h5 class="card-title">
                按底下的按鈕建立角色
            </h5>
            <NavLink class="btn btn-primary mb-3" href="RolesManagement/CreateRole" Match="NavLinkMatch.All">
                新增角色
            </NavLink>
        </div>
    </div>
}
然後去NavMenu.razor加入 NavLink 通往角色管理。
                <li class="nav-item px-3">
                    <NavLink class="nav-link" href="RolesManagement/RolesList" Match="NavLinkMatch.All">
                        <span class="bi bi-kanban-fill h4 p-2 mb-0" aria-hidden="true"></span> Roles
                    </NavLink>
                </li>
這時候開啟網站可以看到這樣的畫面,我們來加上新增角色的畫面並新增一個角色 Admin。

CreateRole.razor.cs
using BlazorServer.Services;
using BlazorServer.ViewModels;
using Microsoft.AspNetCore.Components;
using System.Threading.Tasks;
namespace BlazorServer.Pages.RolesManagement
{
    public partial class CreateRole
    {
        [Inject] protected IRolesRepository RolesRepository { get; set; }
        [Inject] protected NavigationManager NavigationManager { get; set; }
        public CustomRoleViewModel Role { get; set; } = new();
        private async Task createRole()
        {
            await RolesRepository.CreateRoleAsync(Role);
            NavigationManager.NavigateTo("/RolesManagement/RolesList");
        }
    }
}
CreateRole.razor
@page "/RolesManagement/CreateRole"
@attribute [Authorize]
<EditForm class="mt-3" Model="Role" OnValidSubmit="createRole">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <div class="form-group row">
        <label for="RoleName" class="col-sm-1 col-form-label">角色名稱</label>
        <div class="col-sm-3">
            <InputText @bind-Value="Role.RoleName" id="RoleName" class="form-control" placeholder="角色名稱"></InputText>
        </div>
    </div>
    <div class="form-group row">
        <div class="col-sm-10">
            <button type="submit" class="btn btn-primary">
                建立角色
            </button>
        </div>
    </div>
</EditForm>
有了建立功能就要有編輯功能,編輯完成或取消都直接跳轉回角色列表。
EditRole.razor.cs
using BlazorServer.Services;
using BlazorServer.ViewModels;
using Microsoft.AspNetCore.Components;
using System.Threading.Tasks;
namespace BlazorServer.Pages.RolesManagement
{
    public partial class EditRole
    {
        [Inject] protected IRolesRepository RolesRepository { get; set; }
        [Inject] protected NavigationManager NavigationManager { get; set; }
        public CustomRoleViewModel Role { get; set; } = new();
        [Parameter]
        public string RoleId { get; set; }
        protected override async Task OnInitializedAsync()
        {
            var result = await RolesRepository.GetRoleAsync(RoleId);
            Role = new CustomRoleViewModel
            {
                RoleId = result.RoleId,
                RoleName = result.RoleName,
                Users = result.Users
            };
        }
        private async Task editRole()
        {
            await RolesRepository.EditRoleAsync(Role);
            NavigationManager.NavigateTo("/RolesManagement/RolesList");
        }
        public void Cancel()
        {
            NavigationManager.NavigateTo($"/RolesManagement/RolesList");
        }
    }
}
EditRole.razor
@page "/RolesManagement/EditRole/{RoleId}"
@attribute [Authorize]
<EditForm class="mt-3" Model="Role" OnValidSubmit="editRole">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <div class="form-group row">
        <label for="RoleName" class="col-sm-1 col-form-label">角色名稱</label>
        <div class="col-sm-3">
            <InputText @bind-Value="Role.RoleName" id="RoleName" class="form-control" placeholder="角色名稱"></InputText>
        </div>
    </div>
    <div class="card mb-3 w-50">
        <div class="card-header">
            <h3>角色底下的使用者</h3>
        </div>
        <div class="card-body">
            @if (Role.Users.Any())
            {
                foreach (var user in Role.Users)
                {
                    <h5 class="card-title">@user</h5>
                }
            }
            else
            {
                <h5 class="card-title">目前該角色沒有指派給任何使用者</h5>
            }
        </div>
        <div class="card-footer">
            <button type="submit" class="btn btn-primary">更新角色</button>
            <button type="button" class="btn btn-danger" @onclick="Cancel">取消</button>
        </div>
    </div>
</EditForm>
角色 CRUD 功能大概就是這些,筆者只是用最簡單的方式處理,不過專案通常不會這麼簡單,還有其他細微功能要調整,明天來說明如何管理角色底下的使用者,以及如何套用角色授權。
Ref:Creating roles in asp net core
Ref:Get list of roles in asp net core